package com.ruoyi.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;

import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
public class RedisUtil {

	/**
	 * 释放锁脚本，原子操作，lua脚本
	 */
	private static final String UNLOCK_LUA;
	/**
	 * 默认过期时间(100ms)s
	 */
	private static final long DEFAULT_EXPIRE = 1000L;
	
	static {
		StringBuilder sb = new StringBuilder();
		sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
		sb.append("then ");
		sb.append("    return redis.call(\"del\",KEYS[1]) ");
		sb.append("else ");
		sb.append("    return 0 ");
		sb.append("end ");
		UNLOCK_LUA = sb.toString();
	}

    public static class Singleton {
        static RedisUtil instance;

        public static void instanceRedisTemplate(RedisTemplate<String, String> tpl) {
            instance = new RedisUtil(tpl);
        }
    }

    public static <T> RedisUtil getInstance() {
        return Singleton.instance;
    }

    public RedisUtil(RedisTemplate<String, String> tpl) {
        this.redisTemplate = tpl;
    }

    public RedisTemplate<String, String> cloneRedisTemplate() {
        RedisTemplate<String, String> tpl = new RedisTemplate<String, String>();
        tpl.setConnectionFactory(this.redisTemplate.getConnectionFactory());
        tpl.setDefaultSerializer(this.redisTemplate.getDefaultSerializer());
        tpl.setHashKeySerializer(this.redisTemplate.getHashKeySerializer());
        tpl.setHashValueSerializer(this.redisTemplate.getHashValueSerializer());
        tpl.setStringSerializer(this.redisTemplate.getStringSerializer());
        tpl.setValueSerializer(this.redisTemplate.getValueSerializer());
        tpl.afterPropertiesSet();
        return tpl;
    }

    private RedisTemplate<String, String> redisTemplate;

    public void set(String key, String val) {
        BoundValueOperations<String, String> opt = redisTemplate.boundValueOps(key);
        opt.set(val);
    }

    /**
     * @param key
     * @param val
     * @param time s
     */
    public void set(String key, String val, long time) {
        BoundValueOperations<String, String> opt = redisTemplate.boundValueOps(key);
        if (time < 0) {
            opt.set(val);
            return;
        }
        redisTemplate.boundValueOps(key).set(val, time, TimeUnit.SECONDS);
    }

    public String getValue(String key) {
        return redisTemplate.boundValueOps(key).get();
    }

    /**
     * 发送消息
     *
     * @param channel
     * @param message
     */
    public void convertAndSend(String channel, String message) {

        this.redisTemplate.convertAndSend(channel, message);
    }

    public boolean hasKey(String key) {
        return this.redisTemplate.hasKey(key);
    }

    /**
     * 设置Hash值
     *
     * @param key
     * @param propName
     * @param propValue
     * @param timeout
     */
    public void putHash(String key, String propName, Object propValue) {
        BoundHashOperations<String, Object, Object> hashOpt = this.redisTemplate.boundHashOps(key);
        hashOpt.put(propName, propValue);
    }

    public void putHashs(String key, Map<Object, Object> values) {
        BoundHashOperations<String, Object, Object> hashOpt = this.redisTemplate.boundHashOps(key);
        hashOpt.putAll(values);
    }

    public BoundHashOperations<String, Object, Object> initHash(String key, long timeout) {
        BoundHashOperations<String, Object, Object> hashOpt = this.redisTemplate.boundHashOps(key);
        if (timeout > 0) {
            // hashOpt.expire(timeout, TimeUnit.SECONDS);
            expire(key, timeout);
        }
        return hashOpt;
    }

    public Object getHash(String key, String propName) {
        BoundHashOperations<String, Object, Object> hashOpt = this.redisTemplate.boundHashOps(key);
        Object obj = hashOpt.get(propName);
        return obj;
    }

    public long incr(String key, long incrNum) {
        BoundValueOperations<String, String> opt = this.redisTemplate.boundValueOps(key);
        return opt.increment(incrNum);
    }

    public Map<Object, Object> getHash(String key) {
        BoundHashOperations<String, Object, Object> hashOpt = this.redisTemplate.boundHashOps(key);
        Map<Object, Object> obj = hashOpt.entries();
        return obj;
    }

    public Long deleteHash(String key, String prop) {
        BoundHashOperations<String, Object, Object> hashOpt = this.redisTemplate.boundHashOps(key);
        return hashOpt.delete(prop);
    }

    public void delete(String key) {
        this.redisTemplate.delete(key);
    }

    /**
     * 设置超时
     *
     * @param key
     * @param timeoutSeconds
     */
    @Deprecated
    public void expireValue(String key, Long timeoutSeconds) {
        expire(key, timeoutSeconds);
    }

    public Boolean expire(String key, Long timeoutSeconds) {
        return this.redisTemplate.expire(key, timeoutSeconds, TimeUnit.SECONDS);
    }

    public Long getExpire(String key) {
        return this.redisTemplate.getExpire(key);
    }

    public void expireAt(String key, Date date) {
        this.redisTemplate.expireAt(key, date);
    }

    /**
     * 设置值，如不存在的话
     *
     * @param key
     * @param value
     * @param timeoutSeconds
     * @return
     */
    public boolean setnx(String key, String value, long timeoutSeconds) {
        return this.redisTemplate.boundValueOps(key).setIfAbsent(value, timeoutSeconds, TimeUnit.SECONDS);
    }

    public Long leftPush(String key, String value) {
        return this.redisTemplate.boundListOps(key).leftPush(value);
    }

    public Long rightPush(String key, String value) {
        return this.redisTemplate.boundListOps(key).rightPush(value);
    }

    public String leftPop(String key) {
        return this.redisTemplate.boundListOps(key).leftPop();
    }

    public String rightPop(String key) {
        return this.redisTemplate.boundListOps(key).rightPop();
    }

    /**
     * @param key
     * @param incr
     * @param fn
     * @return
     */
    public long incrIfAbsent(String key, long timeoutSeconds, long incr, Function<String, Long> fn) {
        return this.redisTemplate.execute(new RedisCallback<Long>() {

            @Override
            public Long doInRedis(RedisConnection conn) throws DataAccessException {
                byte[] keyBytes = key.getBytes();
                long tmp = conn.incrBy(keyBytes, incr);
                if (timeoutSeconds > 0) {
                    conn.expire(keyBytes, timeoutSeconds);
                }
                if (tmp == incr) { // 如果等于原值，可能未初始化
                    Expiration expiration = Expiration.persistent();
                    Long value = fn.apply(key);
                    boolean flag = conn.set(keyBytes, value.toString().getBytes(), expiration, SetOption.ifPresent());
                    if (!flag) {
                        throw new RuntimeException("set redis value error[key:" + key + "]");
                    }
                    return conn.incrBy(keyBytes, incr);
                } else {
                    return tmp;
                }
            }

        });
    }

    public long incrIfAbsent(String key, long incr, Function<String, Long> fn) {
        return incrIfAbsent(key, -1, incr, fn);
    }

    public boolean deletes(Collection<String> keys) {
        return this.redisTemplate.execute(new RedisCallback<Long>() {

            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                byte[][] rs = new byte[keys.size()][];
                int index = 0;
                for (String key : keys) {
                    rs[index] = key.getBytes();
                    index++;
                }
                return connection.del(rs);
            }

        }) == keys.size();

    }

    /**
     * 获取分布式锁，原子操作
     *
     * @param lockKey   锁
     * @param lockValue 唯一ID, 可以使用UUID.randomUUID().toString();
     * @return 是否枷锁成功
     */
    public boolean lock(String lockKey, String lockValue) {
        return lock(lockKey, lockValue, DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
    }

    /**
     * 获取分布式锁，原子操作
     *
     * @param lockKey   锁
     * @param lockValue 唯一ID, 可以使用UUID.randomUUID().toString();
     * @param expire    过期时间
     * @param timeUnit  时间单位
     * @return 是否枷锁成功
     */
    public boolean lock(String lockKey, String lockValue, long expire, TimeUnit timeUnit) {
        try {
            RedisCallback<Boolean> callback = (connection) -> connection.set(lockKey.getBytes(StandardCharsets.UTF_8),
                    lockValue.getBytes(StandardCharsets.UTF_8), Expiration.seconds(timeUnit.toSeconds(expire)),
                    SetOption.SET_IF_ABSENT);
            return redisTemplate.execute(callback);
        } catch (Exception e) {
            log.error("redis lock error ,lock key: {}, value : {}, error info : {}", lockKey, lockValue, e);
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @param lockKey   锁
     * @param lockValue 唯一ID
     * @return 执行结果
     */
    public boolean unlock(String lockKey, String lockValue) {
        RedisCallback<Boolean> callback = (connection) -> connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1,
                lockKey.getBytes(StandardCharsets.UTF_8), lockValue.getBytes(StandardCharsets.UTF_8));
        return redisTemplate.execute(callback);
    }

}
